﻿using System;
using System.Xml;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements.CWNotation
{
    /// <summary>
    /// An abstract base class for core staff symbols.
    /// </summary>
    public abstract class CoreSymbol : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="CoreSymbol"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The staff that directly owns this symbol.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        internal CoreSymbol(XmlPackagePart packagePart, Staff owner)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            positionAttribute.Value = (0.0).ToString(PackageVocabulary.PackageCulture.NumberFormat);
            staffPositionAttribute.Value = "0";
            typeSizeAttribute.Value = double.NaN.ToString(PackageVocabulary.PackageCulture.NumberFormat);
        }

        /// <summary>
        /// Creates a new <see cref="CoreSymbol"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The staff that directly owns this symbol.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal CoreSymbol(XmlPackagePart packagePart, Staff owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            positionAttribute = element.Attributes[PackageVocabulary.PositionAttribute];
            staffPositionAttribute = element.Attributes[PackageVocabulary.StaffPositionAttribute];
            typeAttribute = element.Attributes[PackageVocabulary.TypeAttribute];
            typeSizeAttribute = element.Attributes[PackageVocabulary.TypeSizeAttribute];
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = CreateSymbolElement(document);

            positionAttribute = document.CreateAttribute(PackageVocabulary.PositionAttribute);
            staffPositionAttribute = document.CreateAttribute(PackageVocabulary.StaffPositionAttribute);
            typeSizeAttribute = document.CreateAttribute(PackageVocabulary.TypeSizeAttribute);
            typeAttribute = document.CreateAttribute(PackageVocabulary.TypeAttribute);

            typeAttribute.Value = GetType().FullName;

            element.Attributes.Append(positionAttribute);
            element.Attributes.Append(staffPositionAttribute);
            element.Attributes.Append(typeSizeAttribute);
            element.Attributes.Append(typeAttribute);

            return element;
        }

        #endregion

        #region Events

        /// <summary>
        /// Occurs when a change occured to this symbol that affected the staff environment.
        /// </summary>
        public event EventHandler EnvironmentChanged;

        #endregion

        #region Protected Methods

        /// <summary>
        /// When overriden in a derived class, this method creates the XML element that
        /// will holds the data of the symbol.
        /// </summary>
        /// <remarks>
        /// The attributes for the inherited properties and the type of the class
        /// will be added by the base class.
        /// </remarks>
        /// <param name="document">
        /// <para>
        /// Type: <see cref="XmlDocument"/>
        /// </para>
        /// <para>
        /// The XML document in which the element is created.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The created element.
        /// </para>
        /// </returns>
        protected abstract XmlElement CreateSymbolElement(XmlDocument document);

        #endregion

        #region Public Methods

        /// <summary>
        /// When overriden in a derived class, this method makes necessary updates
        /// to the symbol to reflect the new state of the staff environment.
        /// </summary>
        /// <param name="environment">
        /// <para>
        /// Type: <see cref="Environment"/>
        /// </para>
        /// <para>
        /// The new staff environment.
        /// </para>
        /// </param>
        public abstract void ProcessEnvironmentChange(Environment environment);

        /// <summary>
        /// Raises the <see cref="EnvironmentChanged"/> event on this <see cref="CoreSymbol"/> instance.
        /// </summary>
        public void RaiseEnvironmentChanged()
        {
            if (EnvironmentChanged != null)
                EnvironmentChanged(this, EventArgs.Empty);
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the beat that this symbol belongs to.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="BeatElement"/>
        /// </para>
        /// <para>
        /// The beat that this symbol belongs to.
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// The <paramref name="value"/> is <see langword="null"/>.<br/>
        /// -or-<br/>
        /// The <paramref name="value"/> does not belong to the <see cref="Measure"/>.
        /// </exception>
        /// TODO: Write the validation rules
        public abstract BeatElement Beat
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the effective type size of the symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The effective type size (in centimeters) of the symbol.
        /// It is equal to <see cref="TypeSize"/> if <see cref="TypeSize"/> is different
        /// from <see cref="Double.NaN"/> or <see cref="Staff.Height"/> of the owner <see cref="Staff"/>
        /// otherwise.
        /// </para>
        /// </value>
        public double EffectiveTypeSize
        {
            get
            {
                if (double.IsNaN(TypeSize))
                    return FindOwner<Staff>().Height;
                else
                    return TypeSize;
            }
        }

        /// <summary>
        /// Gets or sets the measure that this symbol belongs to.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// The measure that this symbol belongs to.
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// The <paramref name="value"/> is <see langword="null"/>.<br/>
        /// -or-<br/>
        /// The <paramref name="value"/> is not presented in the owner <see cref="MusicSectionElement"/>.
        /// </exception>
        /// TODO: Write the validation rules
        public abstract MeasureElement Measure
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the position of the symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The offset (in centimeters) of the symbol from the beginning of the system.
        /// </para>
        /// </value>
        public double Position
        {
            get { return double.Parse(positionAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                positionAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                RaisePropertyChanged("Position");
            }
        }

        /// <summary>
        /// Gets or sets the vertical position of the symbol on the staff.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The vertical position of the symbol on the staff.<br/>
        /// It is given as a staff line / space. The value 0 stands for the first (bottom) staff line.
        /// 1 stands for the first space above the first line, -1 stands for the first space bellow.
        /// 2 stands for the second line etc.
        /// </para>
        /// </value>
        public int StaffPosition
        {
            get { return int.Parse(staffPositionAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                staffPositionAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);

                RaisePropertyChanged("StaffPosition");
            }
        }

        /// <summary>
        /// Gets or sets the type size of the symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The type size (in centimeters) of the symbol.<br/>
        /// The type size allows the symbol to have a different size that would correspond
        /// to its owner <see cref="Staff"/>. It is expressed as the height of a <see cref="Staff"/> 
        /// that would correspond to the size of the symbol.<br/>
        /// To express standard size, you can either use the <see cref="Staff.Height"/> of the owner <see cref="Staff"/>
        /// or <see cref="Double.NaN"/> (the second one ensures that the size of the symbol changes with the height
        /// of the owner <see cref="Staff"/> and is the default value).
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// <paramref name="value"/> is equal to or less than 0.0.
        /// </exception>
        /// TODO: Write the validation rules.
        public double TypeSize
        {
            get { return double.Parse(typeSizeAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                ValidateAndThrow("TypeSize", value);

                typeSizeAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                RaisePropertyChanged("TypeSize");
            }
        }

        /// <summary>
        /// Gets the width of the symbol.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The widht (in centimeters) of the symbol.
        /// </para>
        /// </value>
        public abstract double Width
        {
            get;
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Position"/> property.
        /// </summary>
        private XmlAttribute positionAttribute;
        /// <summary>
        /// Holds the value of the <see cref="StaffPosition"/> property.
        /// </summary>
        private XmlAttribute staffPositionAttribute;
        /// <summary>
        /// Holds the XML attribute that holds the full type name.
        /// </summary>
        private XmlAttribute typeAttribute;
        /// <summary>
        /// Holds the value of the <see cref="TypeSize"/> property.
        /// </summary>
        private XmlAttribute typeSizeAttribute;

        #endregion
    }
}
